ALB のターゲットとして VPC Lambda を指定する構成を CDK でやってみた

ALB のターゲットとして VPC Lambda を指定する構成を CDK でやってみた

Clock Icon2021.01.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

テントの中から失礼します、CX事業本部のてんとタカハシです!

ALB のターゲットに VPC Lambda を指定する構成を CDK でやってみたので、記事にしようと思います。

この構成は、Lambda から RDS に繋げたり、固定IP を持った Nat Gateway 経由で外部サービスにアクセスするなどの用途で使えそうです。

ソースコードは下記のリポジトリにも置いてあります。

GitHub - iam326/alb-vpc-lambda-sample-cdk

環境

環境は下記の通りです。

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H2

$ cdk --version
1.85.0 (build 5f44668)

$ yarn --version
1.22.10

$ node --version
v12.19.0

単純な構成

構成図

構成は下記の通りです。ALB と VPC Lambda を単純に紐づけているだけの構成です。

実装

スタックの実装は下記の通りです。ターゲットグループのターゲットとして VPC Lambda を指定します。

import * as cdk from '@aws-cdk/core';
import { Vpc, SubnetType, SecurityGroup, Peer, Port } from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import { LambdaTarget } from '@aws-cdk/aws-elasticloadbalancingv2-targets';
import * as lambda from '@aws-cdk/aws-lambda';

export class AlbVpcLambdaSampleCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const prefix: string = this.node.tryGetContext('projectName');

    const vpc = new Vpc(this, 'Vpc', {
      cidr: '10.0.0.0/16',
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'PrivateSubnet',
          subnetType: SubnetType.PRIVATE,
        },
      ],
      natGateways: 2,
      maxAzs: 2,
    });

    const securityGroup = new SecurityGroup(this, 'SecurityGroup', {
      securityGroupName: `${prefix}-sg`,
      vpc,
    });
    securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(80));

    const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
      vpc,
      vpcSubnets: vpc.selectSubnets({ subnetGroupName: 'PublicSubnet' }),
      loadBalancerName: `${prefix}-alb`,
      internetFacing: true,
      securityGroup,
    });

    const helloWorldFunction = new lambda.Function(this, 'HelloWorldFunction', {
      code: lambda.Code.fromAsset('dist/hello-world'),
      functionName: `${prefix}-hello-world`,
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
      timeout: cdk.Duration.seconds(10),
      memorySize: 128,
      vpc,
    });

    const albTargetGroup = new elbv2.ApplicationTargetGroup(
      this,
      'AlbTargetGroup',
      {
        vpc,
        targetGroupName: `${prefix}-tg`,
        targetType: elbv2.TargetType.LAMBDA,
        targets: [new LambdaTarget(helloWorldFunction)],
      }
    );

    alb.addListener('AlbListener', {
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      defaultTargetGroups: [albTargetGroup],
    });
  }
}

Lambda の実装は下記の通りです。Content-Typeを指定しないとapplication/octet-streamで返してしまうので、application/jsonを指定しています。

export async function handler(): Promise<any> {
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ hello: 'world' }),
  };
}

動作確認

curl で ALB にアクセスすると、期待通りのレスポンスが返ってきました。

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com
{"hello":"world"}           

複数の VPC Lambda でパスルーティングする構成

構成図

構成は下記の通りです。アクセスされた HTTP メソッドとパスによって、振り分け先を変えています。

実装

先ほどの実装をベースに変更点を記載します。

export class AlbVpcLambdaSampleCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {

    ...

    // 新しい Lambda を作成。返す値以外は同じコード。
    const HogeFugaPiyoFunction = new lambda.Function(
      this,
      'HogeFugaPiyoFunction',
      {
        code: lambda.Code.fromAsset('dist/hoge'),
        functionName: `${prefix}-hoge-fuga-piyo`,
        handler: 'index.handler',
        runtime: lambda.Runtime.NODEJS_12_X,
        timeout: cdk.Duration.seconds(10),
        memorySize: 128,
        vpc,
      }
    );
    
    // これは不要なのでコメントアウト
    // const albTargetGroup = new elbv2.ApplicationTargetGroup(
    //   this,
    //   'AlbTargetGroup',
    //   {
    //     vpc,
    //     targetGroupName: `${prefix}-tg`,
    //     targetType: elbv2.TargetType.LAMBDA,
    //     targets: [new LambdaTarget(helloWorldFunction)],
    //   }
    // );

    // defaultTargetGroups ではなく、defaultAction として、404 NotFound を返す設定を入れる
    const listener = alb.addListener('AlbListener', {
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      // defaultTargetGroups: [albTargetGroup],
      defaultAction: elbv2.ListenerAction.fixedResponse(404, {
        contentType: elbv2.ContentType.TEXT_PLAIN,
        messageBody: 'NotFound',
      }),
    });

    // GET /hello であれば、helloWorldFunction に繋ぐ
    listener.addTargets('AlbListenerTargetHello', {
      priority: 1,
      conditions: [
        elbv2.ListenerCondition.httpRequestMethods(['GET']),
        elbv2.ListenerCondition.pathPatterns(['/hello']),
      ],
      targets: [new LambdaTarget(helloWorldFunction)],
    });
    
    // POST /hoge であれば、HogeFugaPiyoFunction に繋ぐ
    listener.addTargets('AlbListenerTargetHoge', {
      priority: 2,
      conditions: [
        elbv2.ListenerCondition.httpRequestMethods(['POST']),
        elbv2.ListenerCondition.pathPatterns(['/hoge']),
      ],
      targets: [new LambdaTarget(HogeFugaPiyoFunction)],
    });
  }
}

デプロイすると、ALB のリスナールールは下記の通りになります。

動作確認

指定する HTTP メソッドとパスによって、レスポンスが変わるようになりました。存在しないパスにアクセスした場合はNotFoundを返すようになっています。

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com/hello
{"hello":"world"}

$ curl -X POST xxxxx.ap-northeast-1.elb.amazonaws.com/hoge
{"fuga":"piyo"}

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com/foo
NotFound

おわりに

新年1本目の記事になりました。今月中に残り3本は書こうと思っています、がんばるぞお〜。

今回は以上になります。最後まで読んで頂きありがとうございました!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.